Перейти к основному содержимому

5.05. Обработка null

Разработчику Архитектору

Обработка null

null — это специальное значение, которое означает отсутствие ссылки на объект. Это не число, не пустая строка, не логическое значение, а именно отсутствие значения.

string name = null; // Переменная name не указывает ни на какой объект в памяти

Важно: null может существовать только для ссылочных типов (reference types), потому что они хранят указатель на объект в куче (heap). Значимые типы (value types), как правило, не могут быть null, так как всегда содержат какое-то значение.

Ссылочные типы — это типы, экземпляры которых хранятся в куче (heap), а переменная содержит ссылку (указатель) на этот объект. Именно они могут быть null - string, object, классы, интерфейсы, делегаты, массивы.

Person person = new Person(); // ссылка на объект
Person another = null; // ссылка ни на что не указывает

Значимые типы (value types) хранят сами данные, а не ссылку. Они размещаются на стеке (или в структуре). По умолчанию значимые типы не могут быть null, и это соответственно касается int, double, bool, char, DateTime, struct, enum.

int age = 25; // значение хранится напрямую
int number = null; // ОШИБКА компиляции!

Чтобы сделать значимый тип «nullable» (допускающим значение null), C# предоставляет:

Nullable<T> — обобщённый шаблон:

Nullable<int> age = null;

Nullable<T> — это структура, которая может хранить либо значение типа T, либо null. Имеет два свойства:

  • HasValue — true, если значение задано.
  • Value — само значение (вызовет исключение, если HasValue == false).
Nullable<int> age = 30;
if (age.HasValue)
{
Console.WriteLine($"Возраст: {age.Value}");
}
else
{
Console.WriteLine("Возраст не указан");
}

Сокращённый синтаксис: T?

Начиная с C# 2, можно использовать сокращение:

int? age = null; // эквивалентно Nullable<int>
bool? isActive = true;
DateTime? birthDate = null;

Это синтаксический сахар, компилятор преобразует int?Nullable<int>.

С Nullable<T> можно выполнять соответствующие операции.

Арифметика: если один из операндов null, результат — null.

Сравнения: ==, != работают корректно с null.

int? a = 5;
int? b = null;
int? result = a + b; // result == null

bool isEqual = (a == b); // false
bool isNull = (b == null); // true

Value — опасен, если HasValue == false, выбросит InvalidOperationException.

GetValueOrDefault() — вернёт значение или default(T) (например, 0 для int).

GetValueOrDefault(defaultValue) — вернёт значение или указанное значение по умолчанию.

int? number = null;
int value1 = number.Value; // Исключение!
int value2 = number.GetValueOrDefault(); // 0
int value3 = number.GetValueOrDefault(42); // 42

Условный оператор доступа: ?. (Null-conditional member access, Null-условный оператор) предоставляет безопасный доступ к свойствам и элементам, если они не null:

string name = person?.Name;

Если person != null, то person.Name возвращается.

Если person == null, выражение возвращает null.

Тип результата: string? (ссылочный тип, допускающий null)

Person person = null;
string name = person?.Name; // null
int? length = person?.Name?.Length; // null (двойная проверка)
// Вызов метода
person?.PrintInfo();

// Вызов делегата
Action action = null;
action?.Invoke(); // безопасный вызов

Для безопасного доступа к элементам массива, списка, словаря и т.п. используется условный индексный доступ: ?[]

int[] numbers = GetNumbers();
int? first = numbers?[0]; // null, если numbers == null
Dictionary<string, string> dict = null;
string value = dict?["key"]; // null

Важно: ?[] возвращает T?, если T — значимый тип, иначе T (но с возможным null).

Оператор объединения с null: ?? (Null-coalescing operator) возвращает левый операнд, если он не null, иначе — правый.

string message = input ?? "Значение по умолчанию";

Если input != nullmessage = input

Если input == nullmessage = "Значение по умолчанию"

int count = value ?? 0; // если value == null, используем 0

string displayName = user?.Name ?? "Аноним";

DateTime creationDate = record?.Created ?? DateTime.Now;

Это используется для подстановки значений по умолчанию.

Оператор присваивания объединения с null: ??= (Null-coalescing assignment) появился в C# 8.0. Присваивает правую часть только если левая часть равна null.

List<string> names = null;
names ??= new List<string>(); // инициализируем, если null

Работает только с переменными и полями, нельзя использовать с выражениями вроде person?.Name ??= "Unknown".

private string _cache;
public string Data => _cache ??= LoadFromDatabase();

// Ленивая инициализация
public List<int> Items { get; set; }
// где-то в коде:
Items ??= new List<int>();

До C# 8.0 компилятор не различал, может ли ссылочный тип быть null. Это приводило к частым ошибкам в рантайме. Начиная с C# 8.0, появилась возможность включить статическую проверку null для ссылочных типов.

В .csproj файле можно включить режим Nullable Reference Types, это Null-безопасность на уровне компилятора:

<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

или на уровне файла:

#nullable enable

После этого:

  • string name — не должен быть null (non-nullable reference type)
  • string? name — может быть null (nullable reference type)

Пример:

#nullable enable

string name = null; // ⚠️ Предупреждение компилятора!
string? optional = null; // OK

void PrintName(string name) // параметр не может быть null
{
Console.WriteLine(name.Length); // компилятор уверен: name != null
}

PrintName(null); // ⚠️ Предупреждение!

Компилятор отслеживает инициализацию, проверки на null, присваивания. Поэтому включайте nullable enable в новых проектах, это помогает избежать NullReferenceException ещё на этапе компиляции. И используйте ?., ??, ??= для безопасной работы с null.

Избегайте null там, где можно использовать другие значения - вместо null строки — string.Empty или "", вместо null коллекций — Array.Empty<T>() или new List<T>().